﻿/*
 * SonarAnalyzer for .NET
 * Copyright (C) 2014-2025 SonarSource Sàrl
 * mailto:info AT sonarsource DOT com
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the Sonar Source-Available License for more details.
 *
 * You should have received a copy of the Sonar Source-Available License
 * along with this program; if not, see https://sonarsource.com/license/ssal/
 */

using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using Newtonsoft.Json.Linq;

namespace SonarAnalyzer.SourceGenerators;

[Generator]
[ExcludeFromCodeCoverage]
public class RuleCatalogGenerator : IIncrementalGenerator
{
    private static readonly TimeSpan RegexTimeout = TimeSpan.FromMilliseconds(100);

    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        // IIncrementalGenerator transformers should return types with value equality or use a custom comparer via WithComparer to avoid performance issues.
        // This implementation doesn't do it because it would make the implementation much more complex and performance is not a concern for this generator.
        var jsonFiles = RspecFiles(context, "json");
        var htmlFiles = RspecFiles(context, "html");
        var qualityProfile = RspecFiles(context, "qualityprofile").Collect().Select((x, ct) => CancelableExecute(() => ParseSonarWay(x.Single().GetText().ToString()), ct));
        var filePair = jsonFiles.Combine(htmlFiles.Collect())
            .Select((x, ct) => CancelableExecute(() => (Json: x.Left, Html: x.Right.Single(html => html.Path == Path.ChangeExtension(x.Left.Path, ".html"))), ct));
        var ruleDescriptorArguments = filePair.Combine(qualityProfile).Select((x, ct) => CancelableExecute(() => RuleDescriptorArguments(x.Left.Json, x.Left.Html, x.Right), ct)).Collect();
        var rootNamespace = context.AnalyzerConfigOptionsProvider
            .Select((x, ct) => CancelableExecute(() => x.GlobalOptions.TryGetValue("build_property.rootnamespace", out var rootNamespace) ? rootNamespace : null, ct));
        var source = ruleDescriptorArguments.Combine(rootNamespace).Select((x, ct) => CancelableExecute(() => GenerateSource(x.Right, x.Left), ct));

        context.RegisterSourceOutput(source, (context, source) => context.AddSource("RuleCatalog.g.cs", source));
    }

    private static T CancelableExecute<T>(Func<T> func, CancellationToken ct)
    {
        if (ct.IsCancellationRequested)
        {
            return default;
        }
        return func();
    }

    private static IncrementalValuesProvider<AdditionalText> RspecFiles(IncrementalGeneratorInitializationContext context, string fileType) =>
        context.AdditionalTextsProvider
            .Combine(context.AnalyzerConfigOptionsProvider)
            .Where(x => x.Right.GetOptions(x.Left).TryGetValue("build_metadata.AdditionalFiles.RspecFile", out var value) && value == fileType)
            .Select((x, _) => x.Left);

    private static string[] RuleDescriptorArguments(AdditionalText jsonFile, AdditionalText htmlFile, ImmutableHashSet<string> sonarWay)
    {
        var json = JObject.Parse(jsonFile.GetText().ToString());
        var id = Path.GetFileNameWithoutExtension(jsonFile.Path);
        var html = htmlFile.GetText().ToString();
        return
        [
            Encode(id),
            Encode(json.Value<string>("title")),
            Encode(json.Value<string>("type")),
            Encode(json.Value<string>("defaultSeverity")),
            Encode(json.Value<string>("status")),
            $"SourceScope.{json.Value<string>("scope")}",
            sonarWay.Contains(id).ToString().ToLower(),
            Encode(FirstParagraphText(id, html))
        ];
    }

    private static string GenerateSource(string namespacePrefix, ImmutableArray<string[]> rulesArguments)
    {
        var sb = new StringBuilder();
        sb.AppendLine($$"""
            // <auto-generated/>

            namespace {{namespacePrefix}}.Rspec;

            public static class RuleCatalog
            {
                public static Dictionary<string, RuleDescriptor> Rules { get; } = new()
                {
            """);
        foreach (var arguments in rulesArguments)
        {
            sb.AppendLine($@"        {{ {arguments[0]}, new({string.Join(", ", arguments)}) }},");
        }
        sb.AppendLine("""
                };
            }
            """);
        return sb.ToString();
    }

    private static string Encode(string value) =>
        $@"@""{value.Replace(@"""", @"""""")}""";

    private static string FirstParagraphText(string id, string html)
    {
        var match = Regex.Match(html, "<p>(?<Text>.*?)</p>", RegexOptions.Singleline, RegexTimeout);
        if (match.Success)
        {
            var text = match.Groups["Text"].Value;
            text = Regex.Replace(text, "<[^>]*>", string.Empty, RegexOptions.None, RegexTimeout);
            text = text.Replace("\n", " ").Replace("\r", " ");
            text = Regex.Replace(text, @"\s{2,}", " ", RegexOptions.None, RegexTimeout);
            return WebUtility.HtmlDecode(text);
        }
        else
        {
            throw new NotSupportedException($"Description of rule {id} does not contain any HTML <p>paragraphs</p>.");
        }
    }

    private static ImmutableHashSet<string> ParseSonarWay(string json) =>
        JObject.Parse(json)["ruleKeys"].Values<string>().ToImmutableHashSet();
}
